package com.bullsora.kindlerss.business;

import com.bullsora.kindlerss.business.mail.KindleMailSender;
import com.bullsora.kindlerss.model.*;
import com.bullsora.kindlerss.repository.FeedRepository;
import com.bullsora.kindlerss.repository.GenericOperations;
import com.google.appengine.api.datastore.Text;
import com.google.appengine.api.memcache.Expiration;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.appengine.api.taskqueue.Queue;
import com.google.appengine.api.taskqueue.QueueFactory;
import com.google.appengine.api.taskqueue.TaskOptions;
import com.googlecode.objectify.Key;
import com.sun.syndication.feed.synd.SyndContent;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.io.FeedException;
import com.sun.syndication.io.SyndFeedInput;
import com.sun.syndication.io.XmlReader;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.CronExpression;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.net.URL;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;

import static com.google.appengine.api.taskqueue.TaskOptions.Builder.withUrl;


/**
 * @author sorin.buliarca@iquesint.com
 */
@SuppressWarnings({"unchecked"})
@Component
public class SubscriptionsService {

  public static final Log LOG = LogFactory.getLog(SubscriptionsService.class);
  public static final int FEED_UPDATE_THRESHOLD = 15;
  public static final String FILE_SUFFIX_PATTERN = "EE_HH_mm_dd_MMM";
  public static final String SEND_SUBSCRIPTION_PATTERN = "/subscriptions/%1$s/sendNewEntries";

  private StructuredHtmlGenerator htmlGenerator;

  private KindleMailSender mailSender;

  private GenericOperations genericOperations;

  private FeedRepository feedRepository;

  MemcacheService memcacheService;

  private boolean scheduleTasks = true;

  public void scheduleSubscriptionsSend() {
    List<Subscription> subscriptions = genericOperations.findAll(Subscription.class);
    for (Subscription subscription : subscriptions) {
      /*  schedule a new send only if the last check was missed in order to ensure all subscriptions
      are scheduled for sending. */
      Date nextCheck = subscription.getNextCheck();
      if (nextCheck == null || nextCheck.before(new Date())) {
        scheduleNextSend(subscription);
        genericOperations.persist(subscription);
      }
    }
  }

  private void updateFeed(String url) {
    FeedUrl feedUrl = feedRepository.getFeedUrlByUrl(url);
    if (feedUrl == null) {
      LOG.error("No feed url found with URL " + url);
      return;
    }

    if (!shouldUpdate(feedUrl)) {
      LOG.info(String.format("The feed url %1$s was updated at %2$s. Skipping update", url, feedUrl.getLastUpdated()));
      return;
    }

    LOG.info("Checking new entries for feed " + url);
    try {
      SyndFeed syndFeed = new SyndFeedInput().build(new XmlReader(new URL(url)));
      for (SyndEntry receivedEntry : (List<SyndEntry>) syndFeed.getEntries()) {
        if (!feedRepository.isExistentEntry(receivedEntry.getLink(), url)) {
          addFeedEntry(url, receivedEntry);
        }
      }
      feedUrl.setLastUpdated(new Date());
      genericOperations.persist(feedUrl);
    } catch (FeedException e) {
      LOG.error("Error while reading feed " + feedUrl, e);
    } catch (IOException e) {
      LOG.error("Error while reading feed " + feedUrl, e);
    }
  }

  private boolean shouldUpdate(FeedUrl feedUrl) {
    if (feedUrl.getLastUpdated() == null) {
      return true;
    }
    return System.currentTimeMillis() - feedUrl.getLastUpdated().getTime() > FEED_UPDATE_THRESHOLD * 60 * 1000;
  }

  private void addFeedEntry(String feedUrl, SyndEntry receivedEntry) {
    FeedEntry newFeedEntry = new FeedEntry();
    newFeedEntry.setFeedUrl(feedUrl);
    newFeedEntry.setLink(receivedEntry.getLink());
    newFeedEntry.setTitle(receivedEntry.getTitle());
    newFeedEntry.setAuthor(receivedEntry.getAuthor());
    if (receivedEntry.getDescription() != null) {
      newFeedEntry.setDescription(new Text(receivedEntry.getDescription().getValue()));
    } else if (receivedEntry.getContents() != null && receivedEntry.getContents().size() > 0) {
      newFeedEntry.setDescription(new Text(((SyndContent) receivedEntry.getContents().get(0)).getValue()));
    }

    newFeedEntry.setPublishedDate(receivedEntry.getPublishedDate());
    newFeedEntry.setReceiveDate(new Date());
    LOG.info("Adding new entry " + newFeedEntry);
    genericOperations.persist(newFeedEntry);
  }

  public void sendNewEntries(Long subscriptionId) {
    Subscription subscription = findSubscriptionByKey(subscriptionId);
    if (subscription == null) {
      LOG.warn("Could not find subscription with id " + subscriptionId);
      return;
    }

    sendNewEntries(subscription);
    scheduleNextSend(subscription);
    genericOperations.persist(subscription);
  }

  private void scheduleNextSend(Subscription subscription) {
    User user = genericOperations.find(new Key<User>(User.class, subscription.getUserId()));
    String cronExpr = subscription.getSendCronExpression();
    try {

      CronExpression cronExpression = (CronExpression) memcacheService.get(cronExpr);
      if (cronExpression == null) {
        cronExpression = new CronExpression(cronExpr);
        memcacheService.put(cronExpr, cronExpression, Expiration.byDeltaSeconds(7 * 24 * 3600)); // cache it for 7 days.
      }

      cronExpression.setTimeZone(TimeZone.getTimeZone(user.getTimeZone()));
      Date nextScheduledTime = cronExpression.getNextValidTimeAfter(new Date());

      String url = String.format(SEND_SUBSCRIPTION_PATTERN, subscription.getId());
      if (scheduleTasks) {
        Queue defaultQueue = QueueFactory.getDefaultQueue();
        defaultQueue.add(withUrl(url).method(TaskOptions.Method.GET).etaMillis(nextScheduledTime.getTime()));
      }
      LOG.info("Scheduled next send of url " + url + " at " + nextScheduledTime);
      subscription.setNextCheck(nextScheduledTime);
    } catch (ParseException e) {
      LOG.error("Error while parsing cron expression " + cronExpr + " for subscription " +
          subscription.getPrefix() + ". This subscription won't be checked.");
    }
  }

  private void sendNewEntries(Subscription subscription) {
    LOG.info("Sending new entries for subscription " + subscription.getPrefix());

    updateFeeds(subscription);
    computeNewEntries(subscription);
    if (!subscription.getNonEmptyFeeds().isEmpty()) {
      String html = htmlGenerator.generateHtmlForSubscription(subscription);
      User user = genericOperations.find(new Key<User>(User.class, subscription.getUserId()));

      DateFormat format = new SimpleDateFormat(FILE_SUFFIX_PATTERN);
      format.setTimeZone(TimeZone.getTimeZone(user.getTimeZone()));
      subscription.getUserId();
      String fileName = String.format("%1$s_%2$s.html", subscription.getPrefix(), format.format(new Date()));

      mailSender.sendMessage(html, fileName, user.getKindleAccount());
    } else {
      LOG.info("No new entries to send found for subscription " + subscription.getPrefix());
    }

    subscription.setLastChecked(new Date());
  }

  private void computeNewEntries(Subscription subscription) {
    List<Feed> nonEmptyFeeds = new ArrayList<Feed>();
    for (Feed feed : subscription.getFeeds()) {
      List<FeedEntry> feedNewEntries = feedRepository.getNewEntries(subscription, feed.getUrl());
      if (feedNewEntries != null && !feedNewEntries.isEmpty()) {
        feed.setNewEntries(feedNewEntries);
        nonEmptyFeeds.add(feed);
      }
    }
    subscription.setNonEmptyFeeds(nonEmptyFeeds);
  }

  private void updateFeeds(Subscription subscription) {
    for (Feed feed : subscription.getFeeds()) {
      updateFeed(feed.getUrl());
    }
  }

  private Subscription findSubscriptionByKey(Long subscriptionId) {
    com.googlecode.objectify.Key<Subscription> key = new com.googlecode.objectify.Key<Subscription>(Subscription.class, subscriptionId);
    return genericOperations.find(key);
  }

  public StructuredHtmlGenerator getHtmlGenerator() {
    return htmlGenerator;
  }

  public void setHtmlGenerator(StructuredHtmlGenerator htmlGenerator) {
    this.htmlGenerator = htmlGenerator;
  }

  public KindleMailSender getMailSender() {
    return mailSender;
  }

  public void setMailSender(KindleMailSender mailSender) {
    this.mailSender = mailSender;
  }

  public GenericOperations getGenericOperations() {
    return genericOperations;
  }

  public void setGenericOperations(GenericOperations genericOperations) {
    this.genericOperations = genericOperations;
  }

  public FeedRepository getFeedRepository() {
    return feedRepository;
  }

  public void setFeedRepository(FeedRepository feedRepository) {
    this.feedRepository = feedRepository;
  }

  public boolean isScheduleTasks() {
    return scheduleTasks;
  }

  public void setScheduleTasks(boolean scheduleTasks) {
    this.scheduleTasks = scheduleTasks;
  }

  public MemcacheService getMemcacheService() {
    return memcacheService;
  }

  public void setMemcacheService(MemcacheService memcacheService) {
    this.memcacheService = memcacheService;
  }
}
